package fr.emse.tatiana.replayable;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Composite;
import org.w3c.dom.Node;

import fr.emse.tatiana.TatianaConstants;
import fr.emse.tatiana.display.*;
import fr.emse.tatiana.replayable.analysis.AnalysisEvent;
import fr.emse.tatiana.replayable.analysis.IAnalysis;

public class ReplayableModel implements IReplayableModel, IReplayableSaver {

	public static String DISPLAY_TYPE = "fr.emse.tatiana.createdreplayable";

	//maintain list in sorted state
	//maintain both of these.
	private Vector<ReplayableEvent> unsortedevents;
	private Vector<ReplayableEvent> sortedevents;
	//always keep timestamps vector synched to sortedevents vector
	private Vector<Long> timestamps;

	private Vector<Long> alltimes;

	private List<IAnalysis> analyses;
	private Vector<String> facetnames;
	private Vector<Boolean> facetchangeable;
	private Vector<Integer> facetwidth;
	private IProject p;
	private IFile file;
	private boolean saved = true;

	
	ReplayableModel(IFile file, List<IAnalysis> analyses) {
		this(file.getProject());
		this.file = file;
		this.analyses = analyses;
		DisplayTable dt = new DisplayTable(file.getLocation().toString());
		ReplayableDisplayShower shower = new ReplayableDisplayShower(dt,new Vector<IAnalysis>());

		// TODO deal more elegantly with time and src-anchor facets
		// have a layout where they are not shown
		
		setContents(shower,true);

	}

	/** Creates an empty replayable model associated with a project (or corpus). Used internally and for
	 *  throwaway models (e.g. to generate files for export transformations)
	 * @param p the corpus it belongs to
	 */
	public ReplayableModel(IProject p) {
		sortedevents = new Vector<ReplayableEvent>();
		unsortedevents = new Vector<ReplayableEvent>();
		timestamps = new Vector<Long>();
		facetnames = new Vector<String>();
		facetchangeable = new Vector<Boolean>();
		facetwidth = new Vector<Integer>();
		analyses = new Vector<IAnalysis>();
		this.p = p;
	}

	/** Create a new empty editable replayable
	 * @param name
	 * @param project
	 * @param analyses
	 */
	ReplayableModel(IProject project, List<IAnalysis> analyses) {
		this(project);
		this.analyses = analyses;
		facetnames.add("Begin");
		facetnames.add("End");
		facetnames.add("User");
		facetnames.add("Contents");
		facetchangeable.add(false);
		facetchangeable.add(false);
		facetchangeable.add(true);
		facetchangeable.add(true);
		facetwidth.add(6);
		facetwidth.add(6);
		facetwidth.add(6);
		facetwidth.add(132);
		saved = false;
	}
	
	/* fill this model with the contents of another model of type shower (does this work for type Replayablemodel??? probabyl) */
	public void setContents(IReplayableModel shower, boolean includeAnalysis) {
		// assume the shower has facets time, src-anchor and maybe begin and end
		// if the shower doesn't have begin and end, add them from time. don't
		// put time or src-anchor in the facets
		sortedevents = new Vector<ReplayableEvent>();
		unsortedevents = new Vector<ReplayableEvent>();
		timestamps = new Vector<Long>();
		facetnames = new Vector<String>();
		facetchangeable = new Vector<Boolean>();
		facetwidth = new Vector<Integer>();

		facetnames.add("begin");
		facetnames.add("end");
		facetchangeable.add(false);
		facetchangeable.add(false);
		facetwidth.add(6);
		facetwidth.add(6);
		
		
		
		for (int i=0; i<shower.getFacetCount(); i++) { // add all facets except magic numbers
			if (!includeAnalysis && shower.isAnalysisFacet(i)) continue;
			String name = shower.getFacetName(i);
			if (!name.equals("Date") &&
					!name.equals("time") &&
					!name.equals("Source Anchor") &&
					!name.equals("src-anchor") &&
					!name.equals("begin") &&
					!name.equals("end")) {
				facetnames.add(name);
				facetchangeable.add(true);
				facetwidth.add(132);
			}
			
		}

		for (int i=0; i<shower.getEventCount(); i++) {
			Object elem = shower.getEvent(i);
			Anchor a = shower.getAnchor(elem);
			ReplayableEvent e = new ReplayableEvent(shower.getBeginTime(elem), shower.getLastTime(elem)-shower.getBeginTime(elem), a);
			e.setFacet(0, new DisplayDate(e.getTime()));
			e.setFacet(1, new DisplayDate(e.getEndTime()));
			for (int facet=0; facet<shower.getFacetCount(); facet++) {
				if (!includeAnalysis && shower.isAnalysisFacet(facet)) continue;
				String name = shower.getFacetName(facet);
				if (!name.equals("Date") &&
						!name.equals("time") &&
						!name.equals("Source Anchor") &&
						!name.equals("src-anchor")&&
						!name.equals("begin") &&
						!name.equals("end")) {
					int myfacet = facetnames.indexOf(name);
					// TODO this means we only have string values. not a long lasting solution
					e.setFacet(myfacet, shower.getFacetText(elem, facet));
					e.setFacetColor(myfacet, shower.getForeground(elem, facet));
				}
			}
			unsortedevents.add(e);
		}
		sortedevents.addAll(unsortedevents);
		Collections.sort(sortedevents);
		for (ReplayableEvent re : sortedevents) {
			timestamps.add(re.getTime());
		}
	}
	


	/**
	 * @param time
	 * @return the last event which begins before a given time. return null if none exists
	 */
	public Object lastEventBefore(long time) {
		Long t = new Long(time);
		int first = timestamps.indexOf(t);
		if (first<0) { // doesn't exactly match an event
			for (int i = 0; i<timestamps.size() && (time>timestamps.get(i));i++) {
				first=i;
			}
		}

		int last = first;

		if (first>=0) {			
			try {
				while (t.equals(timestamps.get(last+1))) {
					last++;
				} 
			} catch (IndexOutOfBoundsException e) {
				// not a problem
			}
			return sortedevents.get(last);
		}
		return null;
	}


	/**
	 * @param time
	 * @return a list of events sorted by time which are ongoing
	 */
	public List<Object> currentEvents(SynchronisationEvent event) {

		long begin = event.getBegin();
		long end = begin + event.getDuration();

		List<Object> ret = new Vector<Object>();
		ReplayableEvent last = (ReplayableEvent) lastEventBefore(event.getNow());

		for (int i=0; i<sortedevents.size() && sortedevents.get(i).getTime() <= end; i++) {
			ReplayableEvent e = sortedevents.get(i);
			if (e.getEndTime() >= begin) {
				ret.add(e);
			}
		}

		if (ret.isEmpty() && last!=null) {
			ret.add(last);
		}
		return ret;
	}

	public Object getFacet(Object elem, int facet) {
		ReplayableEvent event = (ReplayableEvent) elem;
		if (facet < facetnames.size()) {
			return event.getFacet(facet);
		} else {
			IAnalysis a = analyses.get(facet-facetnames.size());
			return a.getValueForAnchor(event.getAnchor());
		}

	}

	public void setFacetValue(Object elem, int facet, Object value) {
		ReplayableEvent event = (ReplayableEvent) elem;
	
		
		if (facet < facetnames.size()) {
			if (value!=null && value.equals(event.getFacet(facet))) {
				return;
			}
			event.setFacet(facet, value);
			saved = false;
		} else {
			IAnalysis a = analyses.get(facet-facetnames.size());
			a.setValue(event.getAnchor(), value);
		}
	}

	public boolean canModifyColumn(int facet) {
		if (facet < facetnames.size()) {
			return facetchangeable.get(facet);
		} else {
			return true;
		}
	}

	public Object[] getAnalysedEvents(Set<Anchor> anchors) {
		Vector<ReplayableEvent> ret = new Vector<ReplayableEvent>();
		for (ReplayableEvent event: sortedevents) {
			if (anchors.contains(event.getAnchor())) {
				ret.add(event);
			}
		}
		return ret.toArray(new ReplayableEvent[]{});
	}

	public Object[] getAnalysedEvents(IAnalysis a) {
		return getAnalysedEvents(a.getAnchors());
	}


	public Object[] getAnalysedEvents(AnalysisEvent evt) {
		Set<Anchor> anchors = new HashSet<Anchor>();
		anchors.addAll(evt.getAnchors());
		return getAnalysedEvents(anchors);
	}

	public Color getBackground(Object elem, int col) {
		if (col< facetnames.size()) {
			return null;
		} else {
			return analyses.get(col-facetnames.size()).getBackground(((ReplayableEvent)elem).getAnchor());
		}
	}


	public Color getForeground(Object elem, int col) {
		ReplayableEvent event = (ReplayableEvent) elem;
		if (col< facetnames.size()) {
			return event.getFacetColor(col);
		} else {
			return null;
		}
	}

	public CellEditor getCellEditor(int col, Composite table) {
		if (col<facetnames.size()) {
			return new TextCellEditor(table, SWT.MULTI);
		} else {
			return analyses.get(col-facetnames.size()).getChooser(table); // for analysis columns
		}
	}

	public Object getEvent(int i) {
		return unsortedevents.get(i);
	}
	
	public int getEventCount() {
		return sortedevents.size();
	}

	public int getFacetCount() {
		return analyses.size()+facetnames.size();
	}

	public int getFacetIdFor(IAnalysis a) {
		int ret = analyses.indexOf(a);
		if (ret < 0)
			return ret;
		else
			return facetnames.size() + ret;
	}

	public String getFacetName(int col) {
		if (col<facetnames.size()) {
			return facetnames.get(col);
		} else {
			return analyses.get(col-facetnames.size()).getName(); // for analysis columns
		}
	}
	
	public int getFacetCol(String name) {
		if (facetnames.contains(name)) {
			return facetnames.indexOf(name);
		} else {
			for (int i=0; i<analyses.size(); i++) {
				if (analyses.get(i).getName().equals(name)) {
					return facetnames.size()+i;
				}
			}
		}
		return -1;
	}

	public String getFacetText(Object element, int facet) {
		ReplayableEvent event = (ReplayableEvent) element;
		if (facet < facetnames.size()) {
			try {
				Object val = event.getFacet(facet);
				String ret = val.toString();
				if (val instanceof Date || val instanceof DisplayDate) {
					if (true) {
						ret = ret.substring(11,19);
					}
				} else {
					try {
						Node node = (Node) val;
						ret = Ut.formatXMLNode(node);
					} catch (Exception e) {
						// wasn't a node
					}
				}
				return ret;
			} catch (Exception e) {
				return "";
			}
		} else {
			return analyses.get(facet-facetnames.size()).getTextValueForAnchor(event.getAnchor());
		}
	}

	public int getFontSize() {
		// TODO soon fix font size mess
		return 12;
	}

	public int getPreferredWidth(int facet) {
		// TODO soon fix layout mess
		if (facet<facetnames.size()) {
			return facetwidth.get(facet);
		} else {
			return 10; // for analysis columns
		}
	}

	public List<Long> getEventTimestamps() {
		if (alltimes == null) {
			List<Long> ret = new LinkedList<Long>();
			for (ReplayableEvent e: sortedevents) {
				ret.add(e.getTime());
			}
			for (ReplayableEvent e: sortedevents) {
				if (e.getDuration()!=0) {
					ret.add(e.getEndTime());
					Collections.sort(ret);
				}
			}
			alltimes = new Vector<Long>(ret);
		}
		return alltimes;
	}

	public boolean saveToReplayableFile(String name) {
		DisplayFileWriter writer = new DisplayFileWriter(name,DISPLAY_TYPE);

		for (ReplayableEvent event : unsortedevents) {
			writer.newItem();
			writer.newInfoFromString("src-anchor", event.getAnchor().toString());
			writer.newInfo("time", "", new DisplayDate(event.getTime(),event.getDuration()));
			for (int i=0;i<facetnames.size();i++) {
				if (event.getFacetColor(i)!= null) {
					writer.newInfo(facetnames.get(i), "", new DisplayCell(event.getFacet(i),event.getFacetColor(i)));
				} else {
					writer.newInfo(facetnames.get(i), "", event.getFacet(i));
				}
			}
		}

		writer.close();
		saved = true;
		return true;
	}

	public void doSave() {
		if (saveToReplayableFile(file.getLocation().toString())) {

		}
	}

	public void doSave(String replayablename) {
		IFolder replayables = p.getFolder(TatianaConstants.REPLAYABLES_FOLDER_NAME);

		try {
			replayables.refreshLocal(IResource.DEPTH_INFINITE, null);
			if (!replayables.exists()) {
				replayables.create(false,true,null);
			}
		} catch (CoreException e) {
			// shouldn't happen
			e.printStackTrace();
		}

		IFile file = replayables.getFile(replayablename);
		if (saveToReplayableFile(file.getLocation().toString())) {
			if (!file.exists()) {
				try {
					file.refreshLocal(IResource.DEPTH_ZERO, null);
				} catch (CoreException e) {
					// shouldn't happen
					e.printStackTrace();
				}
			}
			ReplayableModelFactory.releaseEditableModel(this.file); // potentially changing the file name
			this.file = file;
		}
	}

	public boolean hasReplayableFile() {
		return (file!=null);
	}
	
	public Object duplicateEvent(Object elem) {
		ReplayableEvent e = (ReplayableEvent) elem;
		ReplayableEvent newevent = new ReplayableEvent(e.getTime(),e.getDuration(),new Anchor(e.getAnchor().toString()+"<split>1</split>"));
		// TODO generate better uid for new anchor
		// TODO do something about timestamps?
		for (int i=0; i<facetnames.size(); i++) {
			newevent.setFacet(i, e.getFacet(i));
		}
		addEvent(newevent);
		return newevent;
	}

	protected void addEvent(ReplayableEvent event) {
		int spos;
		int upos = unsortedevents.size();
		for (spos=0;spos<sortedevents.size();spos++) {
			if (sortedevents.get(spos).getTime() > event.getTime()) {
				upos = (spos==0) ?
						unsortedevents.indexOf(sortedevents.get(spos)) :
							unsortedevents.indexOf(sortedevents.get(spos-1))+1;
						break;
			}
		}
		unsortedevents.add(upos, event);
		sortedevents.add(spos,event);
		timestamps.add(spos,event.getTime());
		saved = false;
	}
	
	public Object addEvent(SynchronisationEvent e) {
		ReplayableEvent event;
		try {
			event = new ReplayableEvent(e.getBegin(),e.getDuration(),new Anchor(e.getAnchors()));
		} catch (NullPointerException npe) {
			event = new ReplayableEvent(e.getNow(),0,new Anchor(e.getAnchors()));
		}
		event.setFacet(0, new DisplayDate(event.getTime()));
		event.setFacet(1, new DisplayDate(event.getEndTime()));
		for (int i=2; i<facetnames.size(); i++) {
			event.setFacet(i, "");
		}
		addEvent(event);
		return event;
	}

	public long getBeginTime(Object elem) {
		return ((ReplayableEvent) elem).getTime();
	}

	public long getLastTime(Object elem) {
		return ((ReplayableEvent) elem).getEndTime();
	}

	public boolean isSaved() {
		return saved;
	}

	public Anchor getAnchor(Object elem) {
		return ((ReplayableEvent) elem).getAnchor();
	}

	public List<Object> anchoredEvents(SynchronisationEvent event) {
		List<Object> ret = new Vector<Object>();
		for (ReplayableEvent e: sortedevents) {
			Anchor eventa = getAnchor(e);
			for (Anchor a : event.getAnchors()) {
				if (a.subOrSuperSet(eventa)) {
					ret.add(e);
				}
			}
		}
		return ret;
	}
	
	public boolean hasFile() {
		return this.file!=null;
	}

	public void removeEvent(Object e) {
		ReplayableEvent event = (ReplayableEvent) e;
		sortedevents.remove(event);
		unsortedevents.remove(event);
		timestamps.remove(event.getTime());
		saved = false;
	}

	public IProject getProject() {
		return this.p;
	}

	public long getFirstTime() {
		return timestamps.get(0);
	}

	public long getLastTime() {
		return getEventTimestamps().get(getEventTimestamps().size()-1);
	}
	
	public void shiftEventUp(ReplayableEvent e) {
		int idx = unsortedevents.indexOf(e);
		unsortedevents.remove(idx);
		unsortedevents.add(idx-1,e);
		saved = false;
	}
	
	public void shiftEventDown(ReplayableEvent e) {
		int idx = unsortedevents.indexOf(e);
		unsortedevents.remove(idx);
		unsortedevents.add(idx+1,e);
		saved = false;
	}

	public List<IAnalysis> getAnalyses() {
		return analyses;
	}

	public boolean isAnalysisFacet(int col) {
		return !(col < facetnames.size());
	}

	public String getName() {
		try {
			return file.getName();
		} catch (NullPointerException npe) {
			return "Untitled";
		}
	}

	public void close() {
		ReplayableModelFactory.releaseEditableModel(this.file);	
	}

}
